// =============================================
// FD VHS Tape Degradation
// Made by Ubik and Claude 2026
// =============================================
// Adds scanlines, analog noise, chromatic
// aberration and tape wobble for authentic
// cassette aesthetics. Also includes
// saturation crush, black crush, and
// chroma blur (luma-preserving).
// Connect video source to video in 1.
// =============================================

// ISADORA_PLUGIN_DESC("VHS Tape Degradation - scanlines, noise, chromatic aberration, tape wobble, saturation crush, black crush, chroma blur. Full cassette degradation.")

// ISADORA_FLOAT_PARAM(degradation_intensity, dint, 0.0, 1.0, 0.4, "Master strength of the tape degradation effects. Scales scanlines, noise, aberration and wobble.")
// ISADORA_FLOAT_PARAM(scanline_density, scnd, 0.0, 1.0, 0.35, "Density and visibility of horizontal scanlines.")
// ISADORA_FLOAT_PARAM(chroma_shift, chsh, 0.0, 0.03, 0.006, "Strength of RGB channel separation (chromatic aberration).")
// ISADORA_FLOAT_PARAM(tape_wobble, twob, 0.0, 0.04, 0.008, "Vertical warping along image edges - mechanical cassette instability.")
// ISADORA_FLOAT_PARAM(noise_amount, nzam, 0.0, 0.3, 0.06, "Amount of analog luminance noise added on top of the image.")
// ISADORA_FLOAT_PARAM(saturation_crush, satr, 0.0, 1.0, 0.3, "Saturation collapse - saturated colors fade like aged tape.")
// ISADORA_FLOAT_PARAM(black_crush, blkc, 0.0, 1.0, 0.35, "Lift and tint dark areas warm/greenish - VHS black reproduction.")
// ISADORA_FLOAT_PARAM(chroma_blur_width, cbwt, 0.0, 1.0, 0.25, "Horizontal chroma blur width. Simulates VHS low chroma bandwidth. Luma stays sharp.")
// ISADORA_FLOAT_PARAM(luma_sharpness, lmsh, 0.0, 1.0, 1.0, "Sharpness of luma relative to chroma blur. 1 = luma fully unaffected.")

uniform float degradation_intensity;
uniform float scanline_density;
uniform float chroma_shift;
uniform float tape_wobble;
uniform float noise_amount;
uniform float saturation_crush;
uniform float black_crush;
uniform float chroma_blur_width;
uniform float luma_sharpness;
uniform float iTime;
uniform vec3  iResolution;
uniform sampler2D tex0;

vec3 rgb2hsv(vec3 c)
{
    float cmax  = max(c.r, max(c.g, c.b));
    float cmin  = min(c.r, min(c.g, c.b));
    float delta = cmax - cmin;
    float h = 0.0;
    if (delta > 0.0001)
    {
        if      (cmax == c.r) h = mod((c.g - c.b) / delta, 6.0);
        else if (cmax == c.g) h = (c.b - c.r) / delta + 2.0;
        else                  h = (c.r - c.g) / delta + 4.0;
        h /= 6.0;
        if (h < 0.0) h += 1.0;
    }
    float s = (cmax < 0.0001) ? 0.0 : delta / cmax;
    return vec3(h, s, cmax);
}

vec3 hsv2rgb(vec3 c)
{
    float h = c.x * 6.0;
    float s = c.y;
    float v = c.z;
    float i = floor(h);
    float f = h - i;
    float p = v * (1.0 - s);
    float q = v * (1.0 - s * f);
    float t = v * (1.0 - s * (1.0 - f));
    if      (i < 1.0) return vec3(v, t, p);
    else if (i < 2.0) return vec3(q, v, p);
    else if (i < 3.0) return vec3(p, v, t);
    else if (i < 4.0) return vec3(p, q, v);
    else if (i < 5.0) return vec3(t, p, v);
    else               return vec3(v, p, q);
}

float rand(vec2 co)
{
    return fract(sin(dot(co, vec2(12.9898, 78.233))) * 43758.5453);
}

void main()
{
    vec2 uv    = gl_FragCoord.xy / iResolution.xy;
    vec2 texel = 1.0 / iResolution.xy;
    float t    = iTime;

    // --- Tape Wobble ---
    // Shifts each row horizontally. Strongest near top and bottom edges.
    float edgeFactor = 1.0 - abs(uv.y - 0.5) * 2.0;
    edgeFactor = 1.0 - edgeFactor;
    edgeFactor = edgeFactor * edgeFactor;
    float wobble = sin(uv.y * 23.0 + t * 3.1) * sin(uv.y * 7.4 - t * 1.7);
    float xShift = wobble * tape_wobble * edgeFactor * degradation_intensity;
    vec2 warpedUV = vec2(uv.x + xShift, uv.y);

    // --- Chromatic Aberration ---
    // R sampled slightly left, B slightly right
    float shiftAmount = chroma_shift * degradation_intensity;
    float r = texture2D(tex0, vec2(warpedUV.x - shiftAmount, warpedUV.y)).r;
    float g = texture2D(tex0, warpedUV).g;
    float b = texture2D(tex0, vec2(warpedUV.x + shiftAmount, warpedUV.y)).b;
    float a = texture2D(tex0, warpedUV).a;
    vec3 color = vec3(r, g, b);

    // --- Analog Luminance Noise ---
    float noise    = rand(uv + vec2(t * 0.07, t * 0.13));
    float noiseMono = (noise - 0.5) * 2.0;
    color += vec3(noiseMono) * noise_amount * degradation_intensity;
    color  = clamp(color, 0.0, 1.0);

    // --- Chroma Blur ---
    // Asymmetric horizontal low-pass on chroma; luma stays sharp.
    float blurPx = chroma_blur_width * 24.0;

    float w0 = 0.227;
    float w1 = 0.194;
    float w2 = 0.144;
    float w3 = 0.094;
    float w4 = 0.053;
    float wL = 0.6;

    float lumaSharp = dot(color, vec3(0.299, 0.587, 0.114));

    vec3 cR1 = texture2D(tex0, warpedUV + vec2(texel.x * blurPx * 0.5,  0.0)).rgb;
    vec3 cR2 = texture2D(tex0, warpedUV + vec2(texel.x * blurPx * 1.0,  0.0)).rgb;
    vec3 cR3 = texture2D(tex0, warpedUV + vec2(texel.x * blurPx * 1.5,  0.0)).rgb;
    vec3 cR4 = texture2D(tex0, warpedUV + vec2(texel.x * blurPx * 2.0,  0.0)).rgb;
    vec3 cL1 = texture2D(tex0, warpedUV - vec2(texel.x * blurPx * 0.25, 0.0)).rgb;
    vec3 cL2 = texture2D(tex0, warpedUV - vec2(texel.x * blurPx * 0.5,  0.0)).rgb;
    vec3 cL3 = texture2D(tex0, warpedUV - vec2(texel.x * blurPx * 0.75, 0.0)).rgb;
    vec3 cL4 = texture2D(tex0, warpedUV - vec2(texel.x * blurPx * 1.0,  0.0)).rgb;

    vec3 chromaBlurred = color    * w0
                       + cR1 * w1 + cR2 * w2 + cR3 * w3 + cR4 * w4
                       + cL1 * (w1 * wL) + cL2 * (w2 * wL)
                       + cL3 * (w3 * wL) + cL4 * (w4 * wL);
    float weightSum = w0 + w1 + w2 + w3 + w4 + (w1 + w2 + w3 + w4) * wL;
    chromaBlurred /= weightSum;

    float lumaBlurred  = dot(chromaBlurred, vec3(0.299, 0.587, 0.114));
    vec3  chrominance  = chromaBlurred - vec3(lumaBlurred);
    float finalLuma    = mix(lumaBlurred, lumaSharp, luma_sharpness);
    color = clamp(vec3(finalLuma) + chrominance, 0.0, 1.0);

    // --- Saturation Crush ---
    // Quadratic curve: highly saturated colors collapse harder.
    vec3 hsv = rgb2hsv(color);
    float crushCurve = hsv.y * hsv.y;
    hsv.y = max(0.0, hsv.y - crushCurve * saturation_crush * 0.7);
    color = hsv2rgb(hsv);

    // --- Black Crush ---
    // Lift and tint shadow areas warm/green - classic worn VHS black level.
    float luma = dot(color, vec3(0.299, 0.587, 0.114));
    float darkMask = clamp(1.0 - luma * 2.0, 0.0, 1.0);
    darkMask = darkMask * darkMask;
    color += vec3(0.08, 0.11, 0.05) * darkMask * black_crush;

    // --- Scanlines ---
    float scanFreq = mix(200.0, 600.0, scanline_density);
    float scanline = sin(gl_FragCoord.y / iResolution.y * scanFreq * 3.14159) * 0.5 + 0.5;
    float scanDim  = mix(1.0, scanline * 0.75 + 0.25, scanline_density * degradation_intensity);
    color *= scanDim;

    color = clamp(color, 0.0, 1.0);
    gl_FragColor = vec4(color, a);
}
